Skip to content

Conversation

@jtydhr88
Copy link
Collaborator

@jtydhr88 jtydhr88 commented Oct 7, 2025

Summary

fix mask editor issues on vueNodes

┆Issue is synchronized with this Notion page by Unito

@jtydhr88 jtydhr88 marked this pull request as draft October 7, 2025 02:06
@dosubot dosubot bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Oct 7, 2025
@github-actions
Copy link

github-actions bot commented Oct 7, 2025

🎨 Storybook Build Status

Build completed successfully!

⏰ Completed at: 10/13/2025, 01:40:41 AM UTC

🔗 Links


🎉 Your Storybook is ready for review!

@github-actions
Copy link

github-actions bot commented Oct 7, 2025

🎭 Playwright Test Results

⚠️ Tests passed with flaky tests

⏰ Completed at: 10/13/2025, 01:55:22 AM UTC

📈 Summary

  • Total Tests: 499
  • Passed: 468 ✅
  • Failed: 0
  • Flaky: 1 ⚠️
  • Skipped: 30 ⏭️

📊 Test Reports by Browser

  • chromium: View Report • ✅ 459 / ❌ 0 / ⚠️ 1 / ⏭️ 30
  • chromium-2x: View Report • ✅ 2 / ❌ 0 / ⚠️ 0 / ⏭️ 0
  • chromium-0.5x: View Report • ✅ 1 / ❌ 0 / ⚠️ 0 / ⏭️ 0
  • mobile-chrome: View Report • ✅ 6 / ❌ 0 / ⚠️ 0 / ⏭️ 0

🎉 Click on the links above to view detailed test results for each browser configuration.

@comfyui-wiki
Copy link
Member

Yes this PR fixed the #5816

Screen.Recording.2025-10-07.at.18.06.18.mov

@christian-byrne christian-byrne added the claude-review Add to trigger a PR code review from Claude Code label Oct 8, 2025
if (node) {
node.imageIndex = currentIndex.value
node.imgs = await Promise.all(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[quality] medium Priority

Issue: Promise rejection not handled - Image loading could fail silently
Context: If an image fails to load in the Promise.all, the entire operation fails but there's no error handling
Suggestion: Add proper error handling with try-catch or Promise.allSettled to handle individual failures gracefully

if (node) {
node.imageIndex = currentIndex.value
node.imgs = await Promise.all(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[performance] medium Priority

Issue: Blocking image loading may freeze UI
Context: Promise.all waits for all images to load synchronously, potentially causing UI freezes with large images or slow networks
Suggestion: Consider loading images asynchronously or showing a loading indicator during this operation

if (props.nodeId) {
const node = app.rootGraph?.getNodeById(props.nodeId)
if (node) {
node.imageIndex = currentIndex.value
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[architecture] medium Priority

Issue: Direct mutation of node object properties
Context: Directly setting node.imageIndex and node.imgs bypasses Vue reactivity and violates immutability principles
Suggestion: Use a reactive store or emit events to parent components to handle state changes properly

if (ComfyApp.clipspace.imgs && node.imgs) {
if (node.images && ComfyApp.clipspace.images) {
// Update node.images even if it's initially undefined (vueNodes mode)
if (ComfyApp.clipspace.images) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[quality] high Priority

Issue: Logic condition removed may cause null reference errors
Context: Removed check for node.images existence before accessing ClipSpace.images could cause undefined/null references
Suggestion: Restore defensive check or add null safety: if (ComfyApp.clipspace.images && node.images)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to remove this confition as under VueNodes, node.images will be undefined and need to be updated

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comprehensive PR Review

This review is generated by Claude. It may not always be accurate, as with human reviewers. If you believe that any of the comments are invalid or incorrect, please state why for each. For others, please implement the changes in one way or another.

Review Summary

PR: fix mask editor bug under vueNodes (#5953)
Impact: 39 additions, 4 deletions across 2 files

Issue Distribution

  • Critical: 0
  • High: 1
  • Medium: 4
  • Low: 1

Category Breakdown

  • Architecture: 1 issue
  • Security: 1 issue
  • Performance: 1 issue
  • Code Quality: 3 issues

Key Findings

Architecture & Design

The PR introduces direct state mutations on node objects, which bypasses Vue's reactivity system. This pattern could lead to inconsistent state management. Consider using proper reactive stores or event-driven communication instead.

Security Considerations

The addition of crossOrigin = 'anonymous' enables cross-origin access to images. While likely necessary for canvas operations, ensure this doesn't expose sensitive images to unauthorized access.

Performance Impact

The synchronous Promise.all for image loading could block the UI thread, especially with large images or slow networks. Consider asynchronous loading patterns or loading indicators.

Integration Points

The changes improve vueNodes integration with the mask editor by properly setting up node state and synchronizing with the node output store. However, removed safety checks could introduce runtime errors.

Positive Observations

  • Good use of TypeScript with proper type annotations
  • Proper async/await pattern for the mask editor function
  • Icon updates follow the project's icon naming conventions (i-lucide:* format)
  • Added proper fallback logic for vueNodes mode

References

Next Steps

  1. Address the high-priority logic issue in app.ts to prevent potential null reference errors
  2. Consider architectural feedback for proper state management patterns
  3. Add error handling for image loading operations
  4. Consider performance optimizations for image loading
  5. Validate CORS security implications

This is a comprehensive automated review. For architectural decisions requiring human judgment, please request additional manual review.

@AustinMroz
Copy link
Collaborator

Accessing nodeOutputs by nodeId instead of nodeLocatorId will cause the code to break when run inside of subgraphs.

But from looking into it, it's a decent amount of work to cleanup the surrounding code to use locatorIds instead of assuming the node resides in the active graph which is kinda out of scope. I can handle it in a separate PR after this one is merged.

Copy link
Contributor

@christian-byrne christian-byrne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the fundamental issue is that, when in Litegraph, both LoadImage and SaveImage have their imgs property assigned when their preview/output is udpated. For SaveImage it is set via this code

export const useNodeImage = (node: LGraphNode, callback?: () => void) => {
node.previewMediaType = 'image'
const loadElement = (url: string): Promise<HTMLImageElement | null> =>
new Promise((resolve) => {
const img = new Image()
img.onload = () => resolve(img)
img.onerror = () => resolve(null)
img.src = url
})
const onLoaded = (elements: HTMLImageElement[]) => {
node.imageIndex = null
node.imgs = elements
callback?.()
}

And useNodeImage is invoked in the draw loop here

const output = nodeOutputStore.getNodeOutputs(this)
const preview = nodeOutputStore.getNodePreviews(this)
const isNewOutput = output && this.images !== output.images
const isNewPreview = preview && this.preview !== preview
if (isNewPreview) this.preview = preview
if (isNewOutput) this.images = output.images
if (isNewOutput || isNewPreview) {
this.animatedImages = output?.animated?.find(Boolean)
const isAnimatedWebp =
this.animatedImages &&
output?.images?.some((img) => img.filename?.includes('webp'))
const isAnimatedPng =
this.animatedImages &&
output?.images?.some((img) => img.filename?.includes('png'))
const isVideo =
(this.animatedImages && !isAnimatedWebp && !isAnimatedPng) ||
isVideoNode(this)
if (isVideo) {
useNodeVideo(this, callback).showPreview()
} else {
useNodeImage(this, callback).showPreview()

But for LoadImage, I guess it is set in the useNodeImageUpload widget logic (which Vue nodes don't have). So, in Vue mode, only the SaveImage is getting node.imgs set. Then, the solution in this PR involves getting the image, initializing the Image object, then setting it manually.

A few questions:

  1. Should this logic go into its own function? Being a side-effect of handleEditMask is fairly confusing and I don't think future maintainers will understand the logic and what is happening
  2. Do we need to create a new Image object? Aren't we doubling the work? Can we not just do this:
<template>
  <img
    v-for="(url, i) in imageUrls"
    :key="i"
    :src="url"
    ref="imageEls"
    class="..."
  />
</template>

<script setup lang="ts">
const imageEls = ref<HTMLImageElement[]>([])

const handleEditMask = async () => {
  if (!props.nodeId) return
  const node = app.rootGraph?.getNodeById(props.nodeId)
  if (!node) return

  node.imageIndex = currentIndex.value
  node.imgs = imageEls.value          // ← already HTMLImageElements
  app.canvas?.selectNode(node)
  await commandStore.execute('Comfy.MaskEditor.OpenMaskEditor')
}
</script>

@jtydhr88 jtydhr88 force-pushed the mask-editor-vueNodes-bugfix branch 3 times, most recently from 7ee4899 to 74adabf Compare October 9, 2025 18:47
@jtydhr88 jtydhr88 force-pushed the mask-editor-vueNodes-bugfix branch from e141dd3 to bee09d5 Compare October 13, 2025 01:38
if (!node) return
node.imageIndex = currentIndex.value
node.imgs = [currentImageEl.value]
app.canvas?.select(node)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is necessary? How can the node be unselected if this is being called?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image Yes, this is necessary, please check the screenshot. In this case, even if no node is selected, the user may still see the icon to open the mask editor when hovering the mouse over the image. However, because no node is currently selected, the mask editor cannot be opened in this situation, since the mask editor requires accessing the selected node.

Copy link
Contributor

@christian-byrne christian-byrne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@christian-byrne
Copy link
Contributor

In follow up (when we have time), we can do:

Add something to keep in sync with LGraphNode.images for compatibility generally, following some pattern similar to

watch(
  () => props.imageUrls,
  (newUrls) => {
    const node = app.graph.getNodeById(props.nodeId)
    if (!node) return

    node.images = [currentImageEl] as string[]
  },
  { deep: true, flush: 'post' }
)

Then we can just remove this, which might be a little bit confusingly named currently:

function updateNodeImages(node: LGraphNode) {
if (!node.images?.length) return
const nodeLocatorId = nodeIdToNodeLocatorId(node.id)
if (nodeLocatorId) {
const existingOutputs = app.nodeOutputs[nodeLocatorId]
if (existingOutputs) {
const updatedOutputs = {
...existingOutputs,
images: node.images
}
app.nodeOutputs[nodeLocatorId] = updatedOutputs
nodeOutputs.value[nodeLocatorId] = updatedOutputs
}
}
}

@christian-byrne christian-byrne merged commit 476d6df into main Oct 15, 2025
26 checks passed
@christian-byrne christian-byrne deleted the mask-editor-vueNodes-bugfix branch October 15, 2025 00:42
@arjansingh arjansingh mentioned this pull request Oct 17, 2025
christian-byrne pushed a commit that referenced this pull request Oct 17, 2025
## Summary

fix mask editor issues on vueNodes

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5953-fix-mask-editor-bug-under-vueNodes-2856d73d3650810aa8a2e1a94c4d97a6)
by [Unito](https://www.unito.io)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:mask-editor area:vue-migration claude-review Add to trigger a PR code review from Claude Code size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vue node: MaskEditor is not loading the right image, and after editing the mask, the mask isn't saved correctly either.

6 participants